package ru.ifmo.itmokis.algo;

import ru.ifmo.itmokis.util.Config;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.LinkedList;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import static java.lang.Math.exp;
import static java.lang.Math.random;
import org.apache.log4j.Logger;
import ru.ifmo.itmokis.sim.Clock;
import ru.ifmo.itmokis.sim.ProductionQueue;
import ru.ifmo.itmokis.domain.Material;
import ru.ifmo.itmokis.domain.Order;
import ru.ifmo.itmokis.domain.OrderedProduct;
import ru.ifmo.itmokis.domain.Product;

/**
 *
 * @author Ivan Stepuk
 */
public class Optimizer {

    private Config config;
    private Clock clock;
    private Normalizer normalizer;
    private ProductionQueue productionQueue;

    public Optimizer(Config config, Clock clock, ProductionQueue productionQueue, Normalizer normalizer) {
        this.config = config;
        this.clock = clock;
        this.productionQueue = productionQueue;
        this.normalizer = normalizer;
    }

    public void optimize() {
        List<OrderedProduct> solution = productionQueue.getPlan();
        List<OrderedProduct> neighbour;
        normalizer.reconfigure(solution.size(), countOrdersWithDeadlines(solution), maxChangeovers(solution));
        double temperature = config.getDouble("InitialTemperature");
        while (temperature > config.getDouble("TemperatureLimit")) {
            for (int i = 0; i < config.getInt("Iterations"); i++) {
                neighbour = neighbour(solution);
                double delta = normalizer.normalize(calculateStats(neighbour))
                        - normalizer.normalize(calculateStats(solution));
                if (delta <= 0 || exp(-delta / temperature) > random()) {
                    solution = neighbour;
                }
            }
            temperature *= config.getDouble("Alpha");
        }
        Logger.getLogger(getClass()).debug("Optimized to solution: " + calculateStats(solution));
        productionQueue.setPlan(solution);
    }

    public Stats calculateStats(List<OrderedProduct> plan) {
        // Criteria A
        int produced = 0;
        Map<Material, Integer> materials = null;
        boolean canProduce = true;
        // Criteria B
        Set<Order> failed = new HashSet<Order>();
        // Criteria C
        int changeovers = 0;

        // Process the first product in the plan...
        OrderedProduct first = plan.get(0);

        // C
        if (productionQueue.getCurrent() != null) {
            updateProductionTime(productionQueue.getCurrent(), first);
            if (Utils.detectChangeover(productionQueue.getCurrent(), first)) {
                changeovers++;
            }
        } else if (productionQueue.getLastProduced() != null) {
            updateProductionTime(productionQueue.getLastProduced(), first);
            if (Utils.detectChangeover(first, productionQueue.getLastProduced())) {
                changeovers++;
            }
        } else {
            first.setStartTime(clock.getCurrentTime());
            first.setFinishTime(first.getStartTime().add(first.getProduct().getProductionDuration()));
        }

        // B
        if (Utils.detectFailedOrder(first)) {
            failed.add(first.getOrder());
        }

        // A
        materials = first.getProduct().getRecipe();
        if (canProduce && productionQueue.enoughMaterialsFor(materials)) {
            produced++;
        } else {
            canProduce = false;
        }

        // Process the rest of the plan...
        for (int i = 1; i < plan.size(); i++) {
            OrderedProduct current = plan.get(i);
            OrderedProduct previous = plan.get(i - 1);

            // C
            updateProductionTime(previous, current);
            if (Utils.detectChangeover(previous, current)) {
                changeovers++;
            }

            // B
            if (Utils.detectFailedOrder(current)) {
                failed.add(current.getOrder());
            }

            // A
            if (canProduce) {
                materials = Utils.sumRecipes(materials, current.getProduct().getRecipe());
                if (productionQueue.enoughMaterialsFor(materials)) {
                    produced++;
                } else {
                    canProduce = false;
                }
            }
        }
        return new Stats(produced, failed.size(), changeovers);
    }

    public List<OrderedProduct> closestNeighbour(List<OrderedProduct> solution) {
        List<OrderedProduct> neighbour = new ArrayList<OrderedProduct>(solution);
        Random rand = new Random();
        int index1 = rand.nextInt(solution.size() - 1);
        int index2 = index1 + 1;
        OrderedProduct temp = neighbour.get(index1);
        neighbour.set(index1, neighbour.get(index2));
        neighbour.set(index2, temp);
        return neighbour;
    }

    public List<OrderedProduct> neighbour(List<OrderedProduct> solution) {
        List<OrderedProduct> neighbour = new ArrayList<OrderedProduct>(solution);
        Random rand = new Random();
        int index1 = rand.nextInt(solution.size());
        int index2 = rand.nextInt(solution.size());
        if (index1 == index2) {
            neighbour(solution);
        }
        OrderedProduct temp = neighbour.get(index1);
        neighbour.set(index1, neighbour.get(index2));
        neighbour.set(index2, temp);
        return neighbour;
    }

    public void updateProductionTime(OrderedProduct fst, OrderedProduct snd) {
        snd.setStartTime(fst.getFinishTime());
        snd.setFinishTime(snd.getStartTime().add(snd.getProduct().getProductionDuration()));
        if (!fst.getProduct().equals(snd.getProduct())) {
            snd.setFinishTime(snd.getFinishTime().add(config.getTime("ChangeoverTime")));
        }
    }

    public int countOrdersWithDeadlines(List<OrderedProduct> solution) {
        Set<Order> orders = new HashSet<Order>();
        for (OrderedProduct op : solution) {
            Order o = op.getOrder();
            if (o.getDeadline() != null) {
                orders.add(o);
            }
        }
        return orders.size() > 0 ? orders.size() : 1;
    }

    public int maxChangeovers(List<OrderedProduct> solution) {
        Map<Product, Integer> numberOfProducts = new HashMap<Product, Integer>();
        for (OrderedProduct op : solution) {
            Product product = op.getProduct();
            if (numberOfProducts.containsKey(product)) {
                numberOfProducts.put(product, numberOfProducts.get(product) + 1);
            } else {
                numberOfProducts.put(product, 1);
            }
        }
        LinkedList<Integer> nums = new LinkedList<Integer>(numberOfProducts.values());
        Collections.sort(nums);
        int theory = solution.size();
        int actual = 2 * (solution.size() - nums.peekLast()) + 1;
        return actual > theory ? theory : actual;
    }
}
